﻿using System;
using System.Windows;
using SW = System.Windows;
using System.Windows.Controls;
using MultiViewer.Network;
using System.Windows.Media;
using System.Threading;
using System.Windows.Input;
using System.Diagnostics;
using System.Windows.Media.Imaging;
using System.IO;
using System.Reflection;
using KML;
using VisualEsse.DataEngine.Types;
using VisualEsse;
using System.Collections.Generic;
using System.Windows.Documents;

namespace MultiViewer.Display.TiledImages
{
    public class TiledImageViewer : Grid
    {
        private InfoWindow Info;
        private Node node;
        private TiledImage image;
        private double PosX;
        private double PosY;
        private Rect nodeVisible;
        private Rect entireVisible;
        private Rect oldNodeVisible;
        private SW.Rect output;
        private SW.Rect oldOutput;

        private bool dragging = false;
        private Point dragStart;

        private KMLDocument kmlDoc = null;
        private List<KMLPlacemark> pushpins = new List<KMLPlacemark>();

        private BitmapImage pushpinImage;
        private string selectedDescr;

        private Rect vePrevRect = new Rect(0, 0, 0, 0);

        public TiledImageViewer()
        {
            VerticalAlignment = VerticalAlignment.Stretch;
            HorizontalAlignment = HorizontalAlignment.Stretch;
            Background = Brushes.Black;
            SnapsToDevicePixels = true;

            string pushPinPath = Path.Combine(
                Path.GetDirectoryName(Path.GetFullPath(Assembly.GetExecutingAssembly().ManifestModule.FullyQualifiedName)),
                "red-pushpin.png");
            pushpinImage = new BitmapImage(new Uri("file://" + pushPinPath));

            try
            {
                kmlDoc = KMLDocument.Load("sample.kml");
                foreach (var folder in kmlDoc.Folders)
                    foreach (KMLPlacemark placemark in folder.Placemarks)
                        pushpins.Add(placemark);
            }
            catch (Exception exc)
            {
                Trace.WriteLine("Failed to load sample.kml: " + exc.Message);
            }
        }

        public TiledImage Image
        {
            get
            {
                return image;
            }
        }

        public MultiViewerWindow MainWindow
        {
            get
            {
                return Parent as MultiViewerWindow;
            }
        }

        #region UI handlers

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            dragging = true;
            dragStart = e.GetPosition(this);
            base.OnMouseLeftButtonDown(e);
        }

        protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
        {
            if (dragging && image != null)
            {
                dragging = false;
                Vector v = e.GetPosition(this) - dragStart;
                double xPan = -v.X * nodeVisible.Width / output.Width;
                double yPan = v.Y * nodeVisible.Height / output.Height;
                Rect newVisible = Utils.Pan(entireVisible, image.EntireImage, xPan, yPan);
                PostSetVisibleRect(newVisible);
            }
            base.OnMouseLeftButtonUp(e);
        }

        protected override void OnMouseRightButtonUp(MouseButtonEventArgs e)
        {
            if (!String.IsNullOrEmpty(selectedDescr))
                PostShowImage(selectedDescr);
            else if (image != null && image is FileTiledImage)
                PostShowImage("@VirtualEarth");
            base.OnMouseRightButtonUp(e);
        }
        
        protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
        {
            base.OnPreviewMouseWheel(e);
            if (node.TransactionCount > 0)
                return;
            Rect newVisible;
            if (e.Delta > 0) // Zoom in
                newVisible = Utils.ZoomIn(entireVisible);
            else
                newVisible = Utils.ZoomOut(entireVisible, image.EntireImage);
            PostSetVisibleRect(newVisible);
        }

        #endregion

        #region Network handlers

        public void AttachNode(Node node)
        {
            this.node = node;
            node.AddTransactionHandler<ShowImage, ShowImageTransactionState>(Dispatcher,
                ShowImagePrepareHandler,
                ShowImageCommitHandler,
                ShowImageRollbackHandler);
            node.AddTransactionHandler<SetVisibleRect, SetVisibleRectTransactionState>(Dispatcher,
                SetVisibleRectPrepareHandler,
                SetVisibleRectCommitHandler,
                SetVisibleRectRollbackHandler);
        }

        public void DetachNode()
        {
            node = null;
            // TODO: Detach handlers
            if (image != null)
                image.Dispose();
        }

        private void ShowImagePrepareHandler(ShowImage request, HandlerResult<ShowImageTransactionState> result)
        {
            dragging = false;
            try
            {
                if (image != null)
                {
                    try
                    {
                        if(image is VirtualEarthImage)
                            vePrevRect = entireVisible;
                        image.Dispose();
                    }
                    catch (Exception exc)
                    {
                        Trace.WriteLine("Error disposing image: " + exc.Message);
                    }
                }
                if (!String.IsNullOrEmpty(request.Image))
                {
                    Rect newVisible;
                    if (request.Image == "@VirtualEarth") {
                        image = new VirtualEarthImage();
                        if(vePrevRect.Width > 0 && vePrevRect.Height > 0)
                            newVisible = vePrevRect;
                        else
                            newVisible = image.EntireImage;
                    } else {
                        image = new FileTiledImage(request.Image);
                        newVisible = image.EntireImage;
                    }
                    ChangeVisibleRect(newVisible);
                }
                Canvas c = RenderImage();
                result.SetSuccess(new ShowImageTransactionState(request,c));
            }
            catch (Exception exc)
            {
                result.SetFailure(exc);
            }
        }

        private void ShowImageCommitHandler(ShowImageTransactionState state)
        {
            ShowImage(state.ImageCanvas);
            StoreCache();
        }

        private void ShowImageRollbackHandler(ShowImageTransactionState state)
        {
            nodeVisible = oldNodeVisible;
            output = oldOutput;
            StoreCache();
        }

        private void SetVisibleRectPrepareHandler(SetVisibleRect request, HandlerResult<SetVisibleRectTransactionState> result)
        {
            dragging = false;
            try 
            {
                ChangeVisibleRect(new Rect(request.Xmin, request.Ymin, request.Xmax, request.Ymax));
                Canvas c = RenderImage();
                result.SetSuccess(new SetVisibleRectTransactionState(request, c));
            }
            catch(Exception exc)
            {
                result.SetFailure(exc);
            }
        }

        private void SetVisibleRectCommitHandler(SetVisibleRectTransactionState state)
        {
            ShowImage(state.ImageCanvas);
            StoreCache();
        }

        private void SetVisibleRectRollbackHandler(SetVisibleRectTransactionState state)
        {
            nodeVisible = oldNodeVisible;
            output = oldOutput;
            StoreCache();
        }

        #endregion

        public Canvas RenderImage()
        {
            Canvas c = new Canvas();
            c.VerticalAlignment = VerticalAlignment.Stretch;
            c.HorizontalAlignment = HorizontalAlignment.Stretch;
            if(image != null) 
            {
                image.Render(c, output, nodeVisible);
                VirtualEarthImage vei = image as VirtualEarthImage;
                if (vei != null)
                    AddPushpins(c, vei);
            }
            return c;
        }
        public void main_Closing(object o, EventArgs e)
        {
            Info.Close();
        }
        public void img_MouseLeftButtonUp(object o, MouseEventArgs e)
        {
            try
            {
                Window main=Application.Current.MainWindow;
                if (Info != null) Info.Close();
                Info = new InfoWindow();
                Info.Left = main.Left + this.PosX;
                Info.Top = main.Top + this.PosY + Info.ActualWidth;
                string infoPath = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(Assembly.GetExecutingAssembly().ManifestModule.FullyQualifiedName)),
                    "info.png");
                BitmapImage myBitmapImage = new BitmapImage(new Uri("file://" + infoPath));
                Info.image1.Source = myBitmapImage;
                Info.textBox1.Text = selectedDescr;
                main.Closing+=new System.ComponentModel.CancelEventHandler(main_Closing);
                Info.Visibility = Visibility.Visible;
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.Message);
            }
            
        }
        public void AddPushpins(Canvas c, VirtualEarthImage vei)
        {
            if (pushpins.Count == 0)
                return;
            try
            {
                int zoom = image.GetZoom(output, nodeVisible);

                GeoRect rect = new GeoRect(
                    VirtualToGeo(nodeVisible.Left, nodeVisible.Bottom, zoom),
                    VirtualToGeo(nodeVisible.Right, nodeVisible.Top, zoom));

                int spotX = 0;
                int spotY = 0;

                double xmin = output.Left;
                double xmax = output.Right;
                double ymin = output.Top;
                double ymax = output.Bottom;

                int tileSizeAtZoom = 256 * (1 << zoom);
                double y1 = tileSizeAtZoom * (-nodeVisible.Bottom + image.EntireImage.Top) / image.EntireImage.Height;
                double y0 = tileSizeAtZoom * (-nodeVisible.Top + image.EntireImage.Top) / image.EntireImage.Height;

                foreach (KMLPlacemark p in pushpins)
                {
                    GeoPoint gp = new GeoPoint((float)p.Point.Latitude, (float)p.Point.Longitude);

                    double tx = (gp.Longitude - rect.LeftLongitude) / rect.WidthLongitude * (xmax - xmin) + xmin;
                    double ty = CoordinateConverter.LatitudeToYAtZoom(gp.Latitude, zoom);
                    ty = (ty - y0) / (y1 - y0) * (ymax - ymin) + ymin;

                    Image img = new Image();
                    img.Source = pushpinImage;

                    string descr = p.Description;
                    string name = p.Name;
                    img.MouseEnter += (o, e) => { selectedDescr = descr; this.PosX = tx - 2*img.ActualWidth+110; this.PosY = ty - 2*img.ActualHeight-110; };
                    img.MouseLeave += (o, e) => selectedDescr = String.Empty;
                    img.MouseLeftButtonUp+=new MouseButtonEventHandler(img_MouseLeftButtonUp);
                    img.ToolTip = name;
                    c.Children.Add(img);
                    Canvas.SetLeft(img, tx);
                    Canvas.SetTop(img, ty);
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine("Exception when rendering pushpins: " + ex.Message);
            }
        }

        private GeoPoint VirtualToGeo(double vx, double vy, int zoom)
        {
            int tileSizeAtZoom = 256 * (1 << zoom);

            float lat;
/*            if (image.Projection == Projection.Linear)
                lat = (float)(-85.0 + 170.0 * (vy - entireImage.Bottom) / entireImage.Height);
            else
            {*/
                double veY = tileSizeAtZoom * (image.EntireImage.Top - vy) / image.EntireImage.Height;
                lat = CoordinateConverter.YToLatitudeAtZoom((int)veY, zoom);
//            }

            float lon = (float)(-180.0 + 360.0 * (vx - image.EntireImage.Left) / image.EntireImage.Width);

            return new GeoPoint(lat, lon);
        }

        public void ShowImage(Canvas c)
        {
            Children.Clear();
            Children.Add(c);
        }

        private void ChangeVisibleRect(Rect entireVisible)
        {
            oldNodeVisible = nodeVisible;
            oldOutput = output;
            this.entireVisible = entireVisible;
            if (ActualWidth <= 0 || ActualHeight <= 0)
                throw new Exception("Width is zero");

            double entireVisibleWidth = entireVisible.Width;
            double entireVisibleHeight = entireVisible.Height;

            double multiDisplayAspect = MainWindow.TotalWidth / (double)MainWindow.TotalHeight;

            double xmin = entireVisible.Left;
            double ymin = entireVisible.Bottom;

            double imageAspect = image.AspectRatio;

            int displayCol = MainWindow.DisplayColumn;
            // TODO: Eliminate this hack
            int displayRow = MainWindow.GridHeight - MainWindow.DisplayRow - 1;

            if (multiDisplayAspect > imageAspect * entireVisibleWidth / entireVisibleHeight) // Multiscreen is wider than image
            {
                double visibleWidth = multiDisplayAspect * entireVisibleHeight / imageAspect;
                xmin -= (visibleWidth - entireVisibleWidth) / 2;

                double clientVisibleHeight = entireVisibleHeight / MainWindow.GridHeight;
                double clientVisibleWidth = visibleWidth / MainWindow.GridWidth;

                // Calculate new client' visible rect
                double clientBottom = ymin + clientVisibleHeight * displayRow;
                double clientTop = ymin + clientVisibleHeight * (displayRow + 1);
                double clientLeft = xmin + clientVisibleWidth * displayCol;
                double clientRight = xmin + clientVisibleWidth * (displayCol + 1);

                double outputLeft = 0, outputWidth = ActualWidth;
                double coeff = ActualWidth / (clientRight - clientLeft);
                if (clientLeft < image.EntireImage.Left)
                {
                    outputLeft = (image.EntireImage.Left - clientLeft) * coeff;
                    outputWidth -= outputLeft;
                    clientLeft = image.EntireImage.Left;
                }
                if (clientRight > image.EntireImage.Right)
                {
                    outputWidth -= (clientRight - image.EntireImage.Right) * coeff;
                    clientRight = image.EntireImage.Right;
                }
                nodeVisible = new Rect(clientLeft, clientBottom, clientRight, clientTop);
                output = new SW.Rect(new Point(outputLeft, 0), new Size(outputWidth, ActualHeight));
            }
            else // Multiscreen is taller than image
            {
                double visibleHeight = entireVisibleWidth * imageAspect / multiDisplayAspect;
                ymin -= (visibleHeight - entireVisibleHeight) / 2;

                double clientVisibleHeight = visibleHeight / MainWindow.GridHeight;
                double clientVisibleWidth = entireVisibleWidth / MainWindow.GridWidth;

                // Calculate new client' rects
                double clientBottom = ymin + clientVisibleHeight * displayRow;
                double clientTop = ymin + clientVisibleHeight * (displayRow + 1);
                double clientLeft = xmin + clientVisibleWidth * displayCol;
                double clientRight = xmin + clientVisibleWidth * (displayCol + 1);

                double outputBottom = 0, outputHeight = ActualHeight;
                double coeff = ActualHeight / (clientTop - clientBottom);
                if (clientBottom < image.EntireImage.Bottom)
                {
                    outputBottom = (image.EntireImage.Bottom - clientBottom) * coeff;
                    outputHeight -= outputBottom;
                    clientBottom = image.EntireImage.Bottom;
                }
                if (clientTop > image.EntireImage.Top)
                {
                    outputHeight -= (clientTop - image.EntireImage.Top) * coeff;
                    clientTop = image.EntireImage.Top;
                }
                nodeVisible = new Rect(clientLeft, clientBottom, clientRight, clientTop);
                output = new SW.Rect(new Point(0, outputBottom), new Size(ActualWidth, outputHeight));
            }
        }

        private void StoreCache()
        {
            if (image != null)
            {
                VirtualEarthImage vei = image as VirtualEarthImage;
                if (vei != null)
                    try
                    {
                        vei.StoreCache();
                    }
                    catch (Exception exc)
                    {
                        Trace.WriteLine("Cannot sync cache: " + exc.Message);
                    }
            }
        }

        private void PostSetVisibleRect(Rect newVisible)
        {
            ThreadPool.QueueUserWorkItem(dummy =>
            {
                node.Perform(new SetVisibleRect
                {
                    Xmin = newVisible.Left,
                    Ymin = newVisible.Bottom,
                    Xmax = newVisible.Right,
                    Ymax = newVisible.Top
                });
            });
        }

        private void PostShowImage(string image)
        {
            ThreadPool.QueueUserWorkItem(dummy =>
            {
                node.Perform(new ShowImage
                {
                    Image = image
                });
            });
        }
    }
}